/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.velocityme.session;

import com.velocityme.entity.ChangeDelta;
import com.velocityme.entity.Contactable;
import com.velocityme.entity.Directory;
import com.velocityme.entity.Node;
import com.velocityme.entity.Role;
import com.velocityme.entity.Search;
import com.velocityme.entity.SearchLevel;
import com.velocityme.entity.State;
import com.velocityme.entity.Activity;
import com.velocityme.entity.ChangeDeltaItem;
import com.velocityme.entity.Transition;
import com.velocityme.enums.ChangeDeltaType;
import com.velocityme.enums.Permission;
import com.velocityme.utility.PermissionCache;
import com.velocityme.utility.SearchEntry;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

/**
 *
 * @author rcrida
 */
@Stateless
public class SearchBean implements SearchLocal {
    @EJB
    private ContactableLocal contactableBean;

    @PersistenceContext
    private EntityManager em;

    private PermissionCache m_permissionCache = PermissionCache.getInstance();

    /**
     * Get a set of all permissions that are currently assigned to the user in the
     * specified node
     **/
    public Set<Permission> getAssignedPermissions(KeyLocal key, Node node) {
        // return an empty set if the node is not specified
        if (node == null)
            return new HashSet<Permission>();
        // return the set from the cache if not null
        Set<Permission> permissions = m_permissionCache.get(key.getUser().getId(), node.getId());
//        if (permissions != null)
//            return permissions;
        // otherwise we need to do the work and look up the permissions
        Search search = createSearchSpace(key, node, false);
        Set<Role> roles = new HashSet<Role>((List<Role>)em.createNamedQuery("findRolesByUserAndSearch")
                                              .setParameter("user", key.getUser())
                                              .setParameter("search", search)
                                              .setParameter("node", node)
                                              .getResultList());
        roles.addAll((List<Role>)em.createNamedQuery("findRolesByGroupAndSearch")
                       .setParameter("user", key.getUser())
                       .setParameter("search", search)
                       .setParameter("node", node)
                       .getResultList());
        removeSearchSpace(key, search);
        permissions = new HashSet<Permission>();
        for (Role role: roles) {
            permissions.addAll(role.getPermissions());
        }
        if (node.equals(key.getUser()) ||
            node.equals(key.getUser()) ||
            node instanceof Directory)
            permissions.add(Permission.NODE_VIEW);
        // record the permissions in the cache for future reference
        m_permissionCache.set(key.getUser().getId(), node.getId(), permissions);
        return permissions;
    }

    /**
     * Search the node tree for the complete set of nodes forming the branch
     * below the specified node.
     *
     * @param p_key User's security key
     * @param p_node The node which is the root of the search
     * @param p_down Is the search recursive or not
     **/
    public Search createSearchSpace(KeyLocal key, Node node, boolean down) {
        return createSearchSpaceFiltered(key, node, new ArrayList<Node>(), down);
    }

    /**
     * Search the node tree for the complete set of nodes forming the branch
     * below the specified node but excluding any in the exclusion set.
     *
     * @param p_key User's security key
     * @param p_node The node which is the root of the search
     * @param p_down Is the search recursive or not
     **/
    //@TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW)
    public Search createSearchSpaceFiltered(KeyLocal key, Node node, Collection<Node> exclusionNodes, boolean down) {
        key.validateKey();
        int level = 0;
        Search search = new Search();
        Collection<SearchLevel> searchLevels = new ArrayList<SearchLevel>();
        Collection<Node> nodes = new ArrayList<Node>();
        nodes.add(node);
        if (down == true) {
            // add all nodes at each level below the current one
            SearchLevel searchLevel = new SearchLevel(search, level++, nodes);
            searchLevels.add(searchLevel);
            do {
                nodes = (List<Node>)em.createNamedQuery("findSubLevel").setParameter("searchLevel", searchLevel).getResultList();
                nodes.removeAll(exclusionNodes);
                searchLevel = new SearchLevel(search, level++, nodes);
                searchLevels.add(searchLevel);
            } while(!nodes.isEmpty());
        }
        else {
            // add the parent node of the current one, in only one level
            Node parentNode = node.getParentNode();
            while (parentNode != null) {
                nodes.add(parentNode);
                parentNode = parentNode.getParentNode();
            }
            SearchLevel searchLevel = new SearchLevel(search, 0, nodes);
            searchLevels.add(searchLevel);
        }
        search.setSearchLevels(searchLevels);
        em.persist(search);
        return search;
    }

    /**
     * Remove the specified node search space from the database when it is no
     * longer required.
     *
     * @param key User's security key
     * @param search The search space to be removed
     **/
    public void removeSearchSpace(KeyLocal key, Search search) {
        em.remove(search);
    }

    public List<Activity> findAllMyResponsibleTasks(KeyLocal key) {
        return (List<Activity>)em.createNamedQuery("findAllMyResponsibleTasks").
                setParameter("contactable", key.getUser().getPerson()).
                getResultList();
    }

    public List<Activity> findAllMyInterestedTasks(KeyLocal key) {
        return (List<Activity>)em.createNamedQuery("findAllMyInterestedTasks").
                setParameter("contactable", key.getUser().getPerson()).
                getResultList();
    }

    public List<Activity> findMyResponsibleTasks(KeyLocal key, Node node) {
        Search search = createSearchSpace(key, node, true);
        List<Activity> tasks;
        try {
            tasks = (List<Activity>)em.createNamedQuery("findMyResponsibleTasks").
                    setParameter("contactable", key.getUser().getPerson()).
                    setParameter("search", search).
                    getResultList();
        } finally {
            removeSearchSpace(key, search);
        }
        return tasks;
    }

    public List<Activity> findMyInterestedTasks(KeyLocal key, Node node) {
        Search search = createSearchSpace(key, node, true);
        List<Activity> tasks;
        try {
            tasks = (List<Activity>)em.createNamedQuery("findMyInterestedTasks").
                    setParameter("contactable", key.getUser().getPerson()).
                    setParameter("search", search).
                    getResultList();
        } finally {
            removeSearchSpace(key, search);
        }
        return tasks;
    }

    public List<Activity> findActiveTasks(KeyLocal key, Node node) {
        Search search = createSearchSpace(key, node, true);
        List<Activity> tasks;
        try {
            tasks = (List<Activity>)em.createNamedQuery("findActiveTasks").
                    setParameter("search", search).
                    getResultList();
        } finally {
            removeSearchSpace(key, search);
        }
        return tasks;

    }

    public List<Activity> findParameterSearch(KeyLocal key, Long nodeId, Set<Long> ids, Set<String> keywords, Date start, Date stop, Set<Long> statusIds, Set<Long> contactableIds) {
        Node node = em.find(Node.class, nodeId);
        Search search = createSearchSpace(key, node, true);
        List<Activity> tasks = new LinkedList<Activity>();

        StringBuffer sql = new StringBuffer();
        sql.append("SELECT DISTINCT task.taskId FROM task ");
        // note that the column names are the wrong way around in searchlevelnode
        sql.append("INNER JOIN searchlevelnode ON task.taskId=searchlevelnode.searchLevelId ");
        sql.append("INNER JOIN searchlevel ON searchlevelnode.nodeId=searchlevel.searchLevelId ");
        sql.append("INNER JOIN node ON task.nodeIdFk=node.nodeId ");
        sql.append("INNER JOIN changedelta ON node.nodeId=changedelta.dBObjectIdFk ");
        sql.append("INNER JOIN changedeltaitem ON changedelta.changeDeltaId=changedeltaitem.changeDeltaIdFk ");
        sql.append("INNER JOIN changedeltatype ON changedelta.changeDeltaTypeIdFk=changedeltatype.changeDeltaTypeId ");
        StringBuffer where = new StringBuffer();
        where.append(composeSearchQuery(search));
        if (ids.size() > 0) {
            if (where.length() > 0) where.append(" AND ");
            where.append(composeIdQuery(ids));
        }
        if (keywords.size() > 0) {
            if (where.length() > 0) where.append(" AND ");
            where.append(composeKeywordQuery(keywords));
        }
        if (start != null || stop != null) {
            if (where.length() > 0) where.append(" AND ");
            where.append(composeDateQuery(start, stop));
        }
        if (statusIds.size() > 0) {
            if (where.length() > 0) where.append(" AND ");
            where.append(composeStatusQuery(statusIds));
        }
        if (contactableIds.size() > 0) {
            if (where.length() > 0) where.append(" AND ");
            where.append(composeContactableQuery(contactableIds));
            // only include tasks that have a responsible contactable in this case
            // note that the column names are the wrong way around in responsiblecontactables
            sql.append("INNER JOIN responsiblecontactables ON task.taskId=responsiblecontactables.contactableId ");
        }
        sql.append(" WHERE ");
        sql.append(where);

        System.out.println(sql.toString());
        for (Long taskId: (List<Long>) em.createNativeQuery(sql.toString()).getResultList()) {
            tasks.add(em.find(Activity.class, taskId));
        }
        removeSearchSpace(key, search);
        return tasks;
    }

    public List<Activity> findCompletedTasks(KeyLocal key, Long nodeId, Date start) {
        Node node = em.find(Node.class, nodeId);
        Search search = createSearchSpace(key, node, true);
        State startState = (State) em.createNamedQuery("findStateStart").getSingleResult();
        List<Activity> tasks = (List<Activity>)em.createNamedQuery("findTasksByState").
                setParameter("search", search).
                setParameter("state", startState).
                getResultList();
        removeSearchSpace(key, search);
        List<Activity> resultTasks = new LinkedList<Activity>();
        // FIXME: that the following implementation is very inefficient and ought to
        // be performed using either SQL or EJBQL.
        for (Activity task: tasks) {
            for (ChangeDelta changeDelta: (List<ChangeDelta>) em.createNamedQuery("findChangeDeltaByNode").setParameter("node", task).getResultList()) {
                if (changeDelta.getCreationTime().after(start)) {
                    resultTasks.add(task);
                    break;
                }
            }
        }
        return resultTasks;
    }

    public List<Activity> findChangedTasks(KeyLocal key, Long nodeId, Date start, Date stop) {
        Node node = em.find(Node.class, nodeId);
        Search search = createSearchSpace(key, node, true);
        List<Activity> tasks = new LinkedList<Activity>();

        StringBuffer sql = new StringBuffer();
        sql.append("SELECT DISTINCT task.taskId FROM task ");
        // note that the column names are the wrong way around in searchlevelnode
        sql.append("INNER JOIN searchlevelnode ON task.taskId=searchlevelnode.searchLevelId ");
        sql.append("INNER JOIN searchlevel ON searchlevelnode.nodeId=searchlevel.searchLevelId ");
        sql.append("INNER JOIN node ON task.nodeIdFk=node.nodeId ");
        sql.append("INNER JOIN changedelta ON node.nodeId=changedelta.dBObjectIdFk ");
        sql.append("INNER JOIN changedeltaitem ON changedelta.changeDeltaId=changedeltaitem.changeDeltaIdFk ");
        sql.append("INNER JOIN changedeltatype ON changedelta.changeDeltaTypeIdFk=changedeltatype.changeDeltaTypeId ");
        StringBuffer where = new StringBuffer();
        where.append(composeSearchQuery(search));
        if (where.length() > 0)
            where.append(" AND ");
        where.append(composeChangeQuery(start, stop));
        sql.append(" WHERE ");
        sql.append(where);

        for (Long taskId: (List<Long>) em.createNativeQuery(sql.toString())) {
            tasks.add(em.find(Activity.class, taskId));
        }
        removeSearchSpace(key, search);
        return tasks;
    }

    public List<Activity> findContactablesResponsibleTasks(KeyLocal key, Long nodeId, Collection<Long> contactableIds, boolean includeDescendents) {
        Node node = em.find(Node.class, nodeId);
        Search search = createSearchSpace(key, node, true);
        List<Activity> tasks = new LinkedList<Activity>();

        Set<Long> allContactableIds = new HashSet<Long>();
        for (Long contactableId: contactableIds) {
            Contactable contactable = em.find(Contactable.class, contactableId);
            allContactableIds.add(contactableId);
            if (includeDescendents) {
                Set<Contactable> descendents = contactableBean.getNestedContactebles(key, contactable);
                for (Contactable descendent: descendents) {
                    allContactableIds.add(descendent.getId());
                }
            }
        }

        StringBuffer sql = new StringBuffer();
        sql.append("SELECT DISTINCT task.taskId FROM task ");
        // note that the column names are the wrong way around in searchlevelnode
        sql.append("INNER JOIN searchlevelnode ON task.taskId=searchlevelnode.searchLevelId ");
        sql.append("INNER JOIN searchlevel ON searchlevelnode.nodeId=searchlevel.searchLevelId ");
        sql.append("INNER JOIN node ON task.nodeIdFk=node.nodeId ");
        // only include tasks that have a responsible contactable in this case
        // note that the column names are the wrong way around in responsiblecontactables
        sql.append("INNER JOIN responsiblecontactables ON task.taskId=responsiblecontactables.contactableId ");
        StringBuffer where = new StringBuffer();
        where.append(composeSearchQuery(search));
        if (where.length() > 0)
            where.append(" AND ");
        where.append(composeContactableQuery(allContactableIds));
        sql.append(" WHERE ");
        sql.append(where);

        for (Long taskId: (List<Long>) em.createNativeQuery(sql.toString())) {
            tasks.add(em.find(Activity.class, taskId));
        }
        removeSearchSpace(key, search);
        return tasks;
    }

    private String composeSearchQuery(Search search) {
        StringBuffer query = new StringBuffer();
        query.append("searchlevel.searchIdFk=");
        query.append(search.getId().toString());
        return query.toString();
    }

    private String composeIdQuery(Set<Long> ids) {
        StringBuffer query = new StringBuffer();
        if (ids.size() > 0) {
            query.append("(");
            boolean first = true;
            for (Long id: ids) {
                if (first)
                    first = false;
                else
                    query.append(" OR ");
                query.append("task.taskId=");
                query.append(id.toString());
            }
            query.append(")");
        }
        return query.toString();
    }

    private String composeKeywordQuery(Set<String> keywords) {
        StringBuffer query = new StringBuffer();
        if (keywords.size() > 0) {
            query.append("(");
            boolean first = true;
            for (String keyword: keywords) {
                String uppercase = keyword.toUpperCase();
                if (first)
                    first = false;
                else
                    query.append(" OR ");
                query.append("(UPPER(node.name) LIKE '%");
                query.append(uppercase);
                query.append("%' OR UPPER(node.description) LIKE '%");
                query.append(uppercase);
                query.append("%' OR UPPER(changedeltaitem.description) LIKE '%");
                query.append(uppercase);
                query.append("%')");
            }
            query.append(")");
        }
        return query.toString();
    }
    
    private String composeDateQuery(java.util.Date p_start, java.util.Date p_stop) {
        StringBuffer query = new StringBuffer();
        if (p_start != null || p_stop != null) {
            query.append("(changedeltatype.name='Create'");
            if (p_start != null) {
                query.append(" AND changedelta.date>'");
                query.append(new SimpleDateFormat("yyyy-MM-dd").format(p_start));
                query.append("'");
            }
            if (p_stop != null) {
                query.append(" AND changedelta.date<'");
                query.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(p_stop));
                query.append("'");
            }
            query.append(")");
        }
        return query.toString();
    }

    private String composeStatusQuery(Set<Long> statusIds) {
        StringBuffer query = new StringBuffer();
        if (statusIds.size() > 0) {
            query.append("(");
            boolean first = true;
            for (Long statusId: statusIds) {
                if (first)
                    first = false;
                else
                    query.append(" OR ");
                query.append("task.statusIdFk=");
                query.append(statusId.toString());
            }
            query.append(")");
        }
        return query.toString();
    }

    private String composeContactableQuery(Set<Long> contactableIds) {
        StringBuffer query = new StringBuffer();
        if (contactableIds.size() > 0) {
            query.append("(");
            boolean first = true;
            for (Long contactableId: contactableIds) {
                if (first)
                    first = false;
                else
                    query.append(" OR ");
                query.append("(responsiblecontactables.taskId=");
                query.append(contactableId.toString());
                query.append(")");
            }
            query.append(")");
        }
        return query.toString();
    }

    private String composeChangeQuery(java.util.Date start, java.util.Date stop) {
        StringBuffer query = new StringBuffer();
        if (start != null || stop != null) {
            query.append("(");
            if (start != null) {
                query.append("changedelta.date>'");
                query.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(start));
                query.append("'");
                if (stop != null)
                    query.append(" AND ");
            }
            if (stop != null) {
                query.append("changedelta.date<'");
                query.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(stop));
                query.append("'");
            }
            query.append(")");
        }
        return query.toString();
    }

    public List<Contactable> getContactablesAssignedToTransition(KeyLocal key, Node node, Transition transition) {
        List<Contactable> results = new ArrayList<Contactable>();
        key.validateKey();
        Search search = createSearchSpace(key, node, false);
//        for (Person person : (List<Person>) em.createNamedQuery("findPersonsBySearchAndTransition").
//                setParameter("search", search).
//                setParameter("transition", transition).
//                getResultList()) {
//            results.add(person);
//        }
//        for (Person person : (List<Person>) em.createNamedQuery("findPersonsBySearchGroupAndTransition").
//                setParameter("search", search).
//                setParameter("transition", transition).
//                getResultList()) {
//            results.add(person);
//        }
        removeSearchSpace(key, search);
        return results;
    }

    public List<SearchEntry> getSearchReport(KeyLocal key, Node rootNode, Collection<Activity> nodeSearchResults, Set<ReportColumns> reportColumns) {
        List<SearchEntry> searchEntries = new ArrayList<SearchEntry>();
        String nodePathName = rootNode.getPathName();
        Pattern pattern = Pattern.compile(nodePathName);
        for (Activity activity : nodeSearchResults) {
            SearchEntry searchEntry = new SearchEntry(activity.getId(), activity.getPriority());
            List<ChangeDelta> created = (List<ChangeDelta>) em.createNamedQuery("findChangeDeltaByType").
                    setParameter("node", activity).
                    setParameter("changeDeltaType", ChangeDeltaType.CREATE).
                    getResultList();
            if (!created.isEmpty()) {
                ChangeDelta changeDelta = created.get(0);
                SearchEntry.Transaction transaction = searchEntry.addTransaction(changeDelta.getChangeDeltaType().name(),
                                                                                 changeDelta);
                transaction.addRow("Name", pattern.matcher(activity.getPathName()).replaceFirst("."));
                transaction.addRow("Description", activity.getDescription());
            }
            searchEntries.add(searchEntry);
            if (!reportColumns.isEmpty()) {
                // get a collection of all the change deltas
                List<ChangeDelta> changeDeltas = (List<ChangeDelta>) em.createNamedQuery("findChangeDeltaByNode").
                        setParameter("node", activity).
                        getResultList();
                for (ChangeDelta changeDelta: changeDeltas) {
                    SearchEntry.Transaction transaction = null;
                    if (reportColumns.contains(ReportColumns.STATE_CHANGES) && changeDelta.getChangeDeltaType() == ChangeDeltaType.CHANGE_STATE)
                        transaction = searchEntry.addTransaction("Change", changeDelta);
                    if (reportColumns.contains(ReportColumns.COMMENTS) && changeDelta.getChangeDeltaType() == ChangeDeltaType.POST_COMMENT)
                        transaction = searchEntry.addTransaction("Comment", changeDelta);
                    if (reportColumns.contains(ReportColumns.RESPONSIBILITY) && changeDelta.getChangeDeltaType() == ChangeDeltaType.REASSIGN_RESPONSIBILITY)
                        transaction = searchEntry.addTransaction("Reassign", changeDelta);
                    if (transaction != null) {
                        for (ChangeDeltaItem changeDeltaItem: changeDelta.getChangeDeltaItems()) {
                            transaction.addRow(changeDeltaItem.getChangeDeltaItemType().name(),
                                               changeDeltaItem.getDescription());
                        }
                    }
                }
            }
        }
        return searchEntries;
    }



}
